Skip to content

[FE] 260224 #241 기능추가출석 출석하기 페이지 수정#273

Closed
sangkyu39 wants to merge 8 commits intomainfrom
260224_#241-기능추가출석-출석하기-페이지-수정

Hidden character warning

The head ref may contain hidden characters: "260224_#241-\uae30\ub2a5\ucd94\uac00\ucd9c\uc11d-\ucd9c\uc11d\ud558\uae30-\ud398\uc774\uc9c0-\uc218\uc815"
Closed

[FE] 260224 #241 기능추가출석 출석하기 페이지 수정#273
sangkyu39 wants to merge 8 commits intomainfrom
260224_#241-기능추가출석-출석하기-페이지-수정

Conversation

@sangkyu39
Copy link
Contributor

@sangkyu39 sangkyu39 commented Mar 3, 2026

출석조회 api 연결
출석조회 목록 항목 수정

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 관리자 대시보드 추가: 회장 및 리더 권한 사용자를 위한 전용 관리 페이지 출시
    • 회원 승인 관리: 대기 중인 회원 가입 신청 검토 및 승인/거절 기능
    • 회원 관리 인터페이스: 기존 회원의 역할, 상태, 권한 변경 및 삭제 기능
    • Excel 일괄 업로드: 엑셀 파일로 회원 데이터 한 번에 등록 가능
    • 개선된 출석 관리: 출석 기록 조회 및 선택 인터페이스 개선
  • 스타일

    • UI/UX 개선: 각 요소의 색상, 패딩, 간격 최적화 및 반응형 디자인 개선

@sangkyu39 sangkyu39 linked an issue Mar 3, 2026 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Mar 3, 2026

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

관리자 인터페이스 대시보드를 구축하기 위해 AdminHome 페이지, AdminHeader/AdminSidebar 컴포넌트, Excel 업로드 기능, 회원 승인/관리 UI를 추가하고, 출석 페이지를 데이터 기반 아키텍처로 리팩토링하며, 관리자 API 유틸리티를 구현했습니다.

Changes

Cohort / File(s) Summary
관리자 인터페이스 - Excel 업로드
frontend/src/components/AdminExcelUpload/AdminExcelUpload.jsx, frontend/src/components/AdminExcelUpload/AdminExcelUpload.module.css
Excel 파일 드래그앤드롭 및 파일 선택기를 지원하는 관리자 Excel 업로드 컴포넌트와 스타일 추가. 업로드 상태, 로딩 스피너, 결과 표시 기능 포함.
관리자 인터페이스 - 헤더 및 사이드바
frontend/src/components/AdminHome/AdminHeader.jsx, frontend/src/components/AdminHome/AdminHeader.module.css, frontend/src/components/AdminHome/AdminSidebar.jsx
관리자 페이지 상단 헤더(검색, 알림, 사용자 버튼 포함), 양쪽 사이드바(로고, 탭 전환, 네비게이션 메뉴 포함) 컴포넌트 추가. 반응형 스타일 포함.
관리자 인터페이스 - 회원 승인 및 관리
frontend/src/components/AdminMemberApproval/AdminMemberApproval.jsx, frontend/src/components/AdminMemberManage/AdminMemberManage.jsx, frontend/src/components/AdminMemberManage/AdminMemberManage.module.css
대기 중인 회원 승인 목록 관리(검색, 개별/일괄 승인/거절 포함), 기존 회원 관리(역할/상태 변경, 선배 전환, 삭제) UI 및 스타일 추가.
관리자 인터페이스 - 대시보드 페이지
frontend/src/pages/AdminHome.jsx, frontend/src/utils/adminHomeData.js
관리자 대시보드 페이지 레이아웃(헤더, 사이드바, 통계, 승인 대기, 활동 로그, 빠른 작업, 회원 패널 포함)과 데이터 페칭 유틸리티 추가.
관리자 API 유틸리티
frontend/src/utils/adminUserApi.js, frontend/src/utils/adminMembersData.js
관리자 사용자 관리 API 함수(조회, 역할/상태 변경, 삭제, Excel 업로드) 2개 모듈로 제공.
출석 페이지 리팩토링
frontend/src/components/attendance/SessionSelectBox.jsx, frontend/src/components/attendance/SessionManage.jsx, frontend/src/components/attendance/SessionManage.module.css, frontend/src/components/attendance/SessionSelectBox.module.css, frontend/src/pages/Attendance.jsx, frontend/src/utils/attendanceList.js, frontend/src/pages/Attendance.module.css
세션 선택 및 관리 컴포넌트를 props 기반 제어 컴포넌트로 변환. 정적 데이터에서 API 기반 동적 데이터 페칭으로 전환. 페이지 제목 및 라벨 변경, 반응형 스타일 업데이트.
사이드바 업데이트
frontend/src/components/Sidebar.jsx
사용자 역할 감지 로직 추가(대통령 역할 확인), 관리자 링크 조건부 렌더링, 네비게이션 라벨 변경("출석하기"→"출석조회").

Sequence Diagram

sequenceDiagram
    actor User
    participant Browser
    participant AdminHome as AdminHome<br/>(Page)
    participant API as Backend API
    participant Components as Child<br/>Components

    User->>Browser: 관리자 페이지 방문
    Browser->>AdminHome: AdminHome 컴포넌트 마운트
    AdminHome->>API: getAdminHomeData() 요청
    API-->>AdminHome: 대시보드 통계, 승인대기, 활동로그, 회원 데이터
    AdminHome->>AdminHome: 상태 업데이트<br/>(dashboardStats, pendingApprovals, etc.)
    AdminHome->>Components: 데이터를 props로 전달
    Components-->>Browser: UI 렌더링 (헤더, 사이드바, 패널)
    User->>Components: 회원 승인/거절 작업
    Components->>API: 승인/거절 API 호출
    API-->>Components: 처리 결과
    Components->>AdminHome: onChanged 콜백 실행
    AdminHome->>API: 데이터 새로고침
    API-->>AdminHome: 업데이트된 데이터
    AdminHome->>Components: UI 업데이트
    Components-->>Browser: 결과 반영
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45분

변경 범위가 광범위하며, 다양한 새로운 컴포넌트(AdminExcelUpload, AdminHeader, AdminSidebar, AdminMemberApproval, AdminMemberManage, AdminHome), 다수의 API 유틸리티 함수, 그리고 기존 attendance 페이지의 아키텍처 전환이 포함되어 있습니다. 각 컴포넌트의 로직 복잡도는 중간 수준이지만, 파일 수의 다양성과 여러 레이어에 걸친 변경으로 인해 검토 난도가 높습니다.

Possibly related issues

Possibly related PRs

Suggested labels

FE

Suggested reviewers

  • DongEun02
  • gxuoo

Poem

🐰 관리자 대시보드, 우아하게 펼쳐지고
Excel 업로드, 문서는 술술 올라가고
회원 승인, 조건 다루며 흐르고
출석 조회, 데이터 중심으로 돌아간다
리팩토링의 마법, 코드는 깨끗해졌네! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning PR 제목은 출석 페이지 수정이라는 주요 변경사항의 일부만 언급하며, 광범위한 관리자 기능 추가(관리자 페이지, 회원 관리, 엑셀 업로드 등)와 여러 API 연동을 포함한 실제 변경사항 전체를 반영하지 못하고 있습니다. PR 제목을 실제 주요 변경사항을 포함하도록 수정하세요. 예: '[FE] 출석 조회 기능 리팩토링 및 관리자 대시보드 구현' 또는 출석 및 관리자 기능을 모두 반영하는 보다 정확한 제목을 제시하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 260224_#241-기능추가출석-출석하기-페이지-수정

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/src/contexts/AuthContext.jsx (2)

27-27: ⚠️ Potential issue | 🟡 Minor

오타: paylaodpayload

변수명에 오타가 있습니다.

✏️ 수정 제안
-    const paylaod = { studentId, password };
-    const res = await api.post('/api/auth/login', paylaod, { signal });
+    const payload = { studentId, password };
+    const res = await api.post('/api/auth/login', payload, { signal });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/contexts/AuthContext.jsx` at line 27, There's a typo in the
variable name "paylaod" in AuthContext.jsx; rename it to "payload" where it's
declared (const paylaod = { studentId, password }) and update all usages of
"paylaod" in the surrounding functions/methods (e.g., any calls or references
that pass this object) to the correct "payload" identifier to avoid
ReferenceErrors.

35-46: ⚠️ Potential issue | 🟠 Major

로그아웃 성공 시 isLoggedIn 상태가 업데이트되지 않습니다.

setIsLoggedIn(false)catch 블록에서만 호출되고 있어, 로그아웃 API가 성공하면 isLoggedIn 상태가 true로 유지됩니다. 이로 인해 UI에서 로그인 상태가 잘못 표시될 수 있습니다.

🐛 수정 제안
 const logout = async () => {
   try {
     await api.post('/api/auth/logout');
+    setIsLoggedIn(false);
   } catch (error) {
     // 로그아웃 API 실패해도 무시 (토큰이 없을 수 있음)
     console.log('로그아웃 API 호출 실패:', error.message);
     setIsLoggedIn(false);
   } finally {
     // localStorage 유저 정보 삭제
     localStorage.removeItem('user');
   }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/contexts/AuthContext.jsx` around lines 35 - 46, The logout
function currently only calls setIsLoggedIn(false) inside the catch block, so on
a successful api.post('/api/auth/logout') the UI stays logged in; move or add
the state update so setIsLoggedIn(false) runs regardless of API outcome (e.g.,
place it in the finally block or immediately after the await) and ensure
localStorage.removeItem('user') remains executed; update the logout function
reference in AuthContext.jsx accordingly so that setIsLoggedIn is always cleared
when logout() completes.
🧹 Nitpick comments (16)
frontend/src/utils/attendanceList.js (1)

3-12: 함수명이 혼란스러울 수 있습니다.

attendanceList는 배열처럼 보이는 이름이지만 실제로는 비동기 함수입니다. fetchAttendanceList 또는 getAttendanceList로 이름을 변경하면 사용하는 쪽에서 await가 필요하다는 것을 명확히 알 수 있습니다.

✏️ 함수명 변경 제안
-export const attendanceList = async () => {
+export const fetchAttendanceList = async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/utils/attendanceList.js` around lines 3 - 12, The exported async
function attendanceList should be renamed to a clearer async name (e.g.,
fetchAttendanceList or getAttendanceList) to signal it returns a Promise; update
the function declaration/export from attendanceList to the chosen name and
update all call sites/imports that reference attendanceList accordingly (search
for attendanceList usages), leaving the request logic
(api.get('/api/attendance/me')) and error handling unchanged.
frontend/src/components/AdminHome/MemberList.jsx (1)

8-25: 인라인 스타일 대신 CSS 모듈 사용을 권장합니다.

다른 Admin 컴포넌트들은 CSS 모듈을 사용하는데, 이 컴포넌트만 인라인 스타일을 사용하고 있어 일관성이 떨어집니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminHome/MemberList.jsx` around lines 8 - 25,
Replace the inline styles in the MemberList component with classNames from a CSS
module: create (or import) a MemberList.module.css and move table, thead, th, td
and tr styles there, then update the JSX in MemberList.jsx to use className
attributes (e.g., className={styles.table}, styles.th, styles.td, styles.row)
instead of the inline style objects used on the <table>, <th>, <td>, and <tr>
elements and keep the members.map rendering logic intact.
frontend/src/components/Sidebar.jsx (1)

33-50: 중복 API 호출: /api/user/details가 이미 AuthContext에서 호출됩니다.

AuthContext가 마운트 시 /api/user/details를 호출하고 있으며, 여기서 다시 동일한 API를 호출합니다. 사용자 역할 정보를 AuthContext에 저장하고 공유하면 불필요한 네트워크 요청을 줄일 수 있습니다.

♻️ AuthContext에서 역할 정보 공유 제안

AuthContext에서 사용자 역할을 저장하고 내보내면 Sidebar와 AdminRoute에서 재사용할 수 있습니다:

// AuthContext.jsx에서
const [userRole, setUserRole] = useState(null);

useEffect(() => {
  const checkLogin = async () => {
    try {
      const { data } = await api.get('/api/user/details');
      setIsLoggedIn(true);
      setUserRole(data?.role || null);
    } catch {
      setIsLoggedIn(false);
      setUserRole(null);
    } finally {
      setLoading(false);
    }
  };
  checkLogin();
}, []);

// Provider value에 userRole 추가
<AuthContext.Provider value={{ isLoggedIn, loading, login, logout, userRole }}>

그런 다음 Sidebar에서:

const { isLoggedIn, userRole } = useAuth();
const isPresident = userRole?.toUpperCase() === 'PRESIDENT';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/Sidebar.jsx` around lines 33 - 50, The Sidebar
currently calls /api/user/details in the useEffect (checkAdminRole) causing
duplicate requests; instead, expose and use the role stored in AuthContext
(e.g., add or use userRole in AuthContext state) and remove the checkAdminRole
effect and api.get from Sidebar; in Sidebar replace the isPresident logic to
derive from the context value (useAuth -> userRole, then compute
userRole?.toUpperCase() === 'PRESIDENT') so only AuthContext performs the
network call.
frontend/src/pages/AdminExcelUpload.module.css (1)

1-33: AdminMemberApproval.module.css와 완전히 동일한 코드입니다.

이 파일은 AdminMemberApproval.module.css와 100% 동일합니다. 공통 레이아웃 스타일을 AdminLayout.module.css로 추출하여 재사용하면 중복을 제거하고 유지보수성을 높일 수 있습니다.

♻️ 공통 스타일 추출 제안
  1. frontend/src/styles/AdminLayout.module.css 생성
  2. 공통 레이아웃 스타일 이동
  3. 각 페이지에서 import하여 사용:
import layoutStyles from '../../styles/AdminLayout.module.css';
// 또는 페이지별 추가 스타일과 병합
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/AdminExcelUpload.module.css` around lines 1 - 33, Extract
the duplicated CSS (.layout, .mainArea, .contentArea and the media queries) from
AdminExcelUpload.module.css and AdminMemberApproval.module.css into a new shared
stylesheet AdminLayout.module.css; move those rules exactly as-is into
AdminLayout.module.css, then update the pages/components that import
AdminExcelUpload.module.css and AdminMemberApproval.module.css to also import
layoutStyles from '../../styles/AdminLayout.module.css' (or the appropriate
relative path) and replace usages of .layout, .mainArea, and .contentArea to use
layoutStyles.layout, layoutStyles.mainArea, layoutStyles.contentArea; keep any
page-specific rules in their original module files (or remove duplicates) so
only common layout rules remain in AdminLayout.module.css.
frontend/src/utils/adminHomeData.js (1)

3-8: recentActivities가 하드코딩된 정적 데이터입니다.

현재 최근 활동 데이터가 정적으로 하드코딩되어 있습니다. 실제 활동 기록 API가 구현되면 동적 데이터로 교체가 필요합니다. 의도된 플레이스홀더라면 TODO 주석을 추가하는 것이 좋습니다.

💡 TODO 주석 추가 제안
+// TODO: 실제 활동 기록 API 연동 필요
 const recentActivities = [
   { id: 1, message: '출석 체크 세션이 생성되었습니다.', time: '10분 전' },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/utils/adminHomeData.js` around lines 3 - 8, The current
recentActivities constant is static placeholder data; either add a clear TODO
comment above recentActivities indicating it must be replaced by the real API
response when available, or replace the constant with an async fetch function
(e.g., fetchRecentActivities or getRecentActivities) that calls your backend
endpoint (e.g., '/api/recent-activities'), parses JSON, returns the array, and
returns a safe fallback (empty array) on error; update exports to expose the
async function and remove/stop using the hardcoded recentActivities constant.
frontend/src/utils/adminUserApi.js (1)

4-38: 관리자 사용자 API가 다른 유틸과 중복되어 유지보수 리스크가 큽니다

frontend/src/utils/adminMembersData.js와 엔드포인트/행위가 대부분 겹칩니다. 한 곳에서 공통 함수를 제공하고 다른 파일은 얇은 래퍼로 두는 구조로 정리해 두면, 추후 파라미터/응답 포맷 변경 시 누락 가능성을 줄일 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/utils/adminUserApi.js` around lines 4 - 38, This file duplicates
endpoints already implemented in adminMembersData.js; refactor by extracting
shared API calls into a single common module (e.g., adminUsersApi or reuse
adminMembersData) and convert functions here—getAdminUsers, updateAdminUserRole,
updateAdminUserStatus, promoteAdminUserSenior, deleteAdminUser—into thin
wrappers that import and re-export or delegate to the canonical implementations;
ensure parameter names and return shapes match the original functions and update
callers to import the shared module to avoid drift when endpoints or response
formats change.
frontend/src/components/AdminHome/PendingApprovalsPanel.jsx (2)

8-31: 중복 클릭으로 승인/거절 요청이 연속 전송될 수 있습니다

Line 54/61 버튼에 처리중 상태 제어가 없어, 빠르게 연타하면 동일 회원에 대한 요청이 여러 번 나갈 수 있습니다. 처리 중에는 버튼 비활성화가 필요합니다.

🔧 제안 코드
+import { useState } from 'react';
 import { Link } from 'react-router-dom';
 import {
   approvePendingMember,
   rejectPendingMember,
 } from '../../utils/adminMemberManageData';
 
 const PendingApprovalsPanel = ({ members = [], styles, onChanged }) => {
+  const [processingId, setProcessingId] = useState(null);
+
   const handleApprove = async (member) => {
+    if (processingId === member.id) return;
     if (!window.confirm(`${member.name}님의 가입을 승인하시겠습니까?`)) {
       return;
     }
 
     try {
+      setProcessingId(member.id);
       await approvePendingMember({ userId: member.id });
       await onChanged?.();
     } catch (error) {
       window.alert(error?.message || '가입 승인 처리에 실패했습니다.');
+    } finally {
+      setProcessingId(null);
     }
   };
 
   const handleReject = async (member) => {
+    if (processingId === member.id) return;
     if (!window.confirm(`${member.name}님의 가입을 거절하시겠습니까?`)) {
       return;
     }
 
     try {
+      setProcessingId(member.id);
       await rejectPendingMember({ userId: member.id });
       await onChanged?.();
     } catch (error) {
       window.alert(error?.message || '가입 거절 처리에 실패했습니다.');
+    } finally {
+      setProcessingId(null);
     }
   };
@@
               <button
                 type="button"
                 className={styles.actionPrimary}
                 onClick={() => handleApprove(member)}
+                disabled={processingId === member.id}
               >
                 승인
               </button>
               <button
                 type="button"
                 className={styles.actionSecondary}
                 onClick={() => handleReject(member)}
+                disabled={processingId === member.id}
               >
                 거절
               </button>

Also applies to: 51-64

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminHome/PendingApprovalsPanel.jsx` around lines 8 -
31, handleApprove and handleReject allow repeated rapid clicks because there is
no "processing" state per member; add a local state (e.g., useState of a Set or
map like processingIds) to track which member IDs are being processed, check and
early-return if the member.id is already in processing, add the member.id to
processingIds before the async call and remove it in a finally block, and update
the approve/reject buttons' disabled prop (and show a spinner if desired) by
checking processingIds.has(member.id) so the UI prevents duplicate requests for
the same member while the request is in-flight.

42-68: 대기 회원이 없을 때 빈 영역만 보이는 UX를 보완하면 좋겠습니다

Line 42~68에서 members.length === 0일 때 안내 문구가 없어 패널이 비어 보입니다. 빈 상태 메시지를 추가하면 관리자가 현재 상태를 더 빨리 이해할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminHome/PendingApprovalsPanel.jsx` around lines 42
- 68, The panel currently renders an empty list when members.length === 0;
update the PendingApprovalsPanel component to show a clear empty-state message
instead of an empty UL: add a conditional branch around the members rendering so
that when members.length === 0 you render a div or paragraph (styled with a new
or existing styles.emptyState) with a short message like "대기 중인 회원이 없습니다" and
optional helper text/CTA; keep the existing list rendering (with members.map,
listItem keys, and handlers handleApprove / handleReject) for the non-empty
case.
frontend/src/components/AdminRoute.jsx (1)

5-8: 관리자 권한 문자열을 라우트 가드 내부 하드코딩 대신 정책 상수로 분리해 주세요

Line 7의 TODO 상태로 배포되면 권한 정책 변경 시 라우팅 실패 리스크가 큽니다. 권한 집합을 상수(또는 서버/환경설정)로 분리하는 편이 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminRoute.jsx` around lines 5 - 8, The isAdminRole
function currently hardcodes the 'PRESIDENT' string; extract that into a
reusable policy constant (e.g., ADMIN_ROLES or ADMIN_ROLE) and reference the
constant from isAdminRole instead of the literal. Create a top-level constant in
the module (or import from a central config/env) such as ADMIN_ROLES =
['PRESIDENT'] (or read from process.env/feature flag) and change isAdminRole to
check membership (use normalizedRole and includes/contains) so role policy can
be updated without editing AdminRoute.jsx.
frontend/src/components/AdminMemberManage/AdminMemberManage.module.css (1)

26-35: 키보드 포커스 가시성을 명시하면 접근성이 더 좋아집니다

검색/필터 입력과 액션 버튼에 :focus-visible 스타일을 추가하면 키보드 사용자에게 현재 포커스 위치가 더 명확해집니다.

🔧 제안 코드
 .actionButton {
   border: 1px solid `#d1d5db`;
   border-radius: 8px;
   background: `#fff`;
   padding: 6px 10px;
   font-size: 12px;
   cursor: pointer;
 }
+
+.searchInput:focus-visible,
+.filterSelect:focus-visible,
+.actionButton:focus-visible {
+  outline: 2px solid `#2563eb`;
+  outline-offset: 2px;
+}

Also applies to: 155-162

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminMemberManage/AdminMemberManage.module.css`
around lines 26 - 35, Add keyboard-focus-visible styles for the interactive form
controls to improve accessibility: update the CSS for .searchInput and
.filterSelect and any action button classes (e.g., .actionButton or the buttons
used in this component) to include a :focus-visible rule that provides a
high-contrast visible indicator (such as a distinct outline or box-shadow and no
outline: none) and preserves existing sizing/spacing; ensure the style is
applied where similar controls exist later in the file (the block referenced by
the review) so keyboard users can clearly see focus state across search, filter
and action controls.
frontend/src/components/AdminExcelUpload/AdminExcelUpload.module.css (1)

116-134: 긴 파일명 말줄임 안정성을 위해 flex 수축 속성을 보강해 주세요.

현재 설정만으로는 긴 파일명이 버튼을 밀어내며 말줄임이 깨질 수 있습니다.

🔧 제안 수정안
 .fileName {
+  flex: 1;
+  min-width: 0;
   font-size: 13px;
   color: `#111827`;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminExcelUpload/AdminExcelUpload.module.css` around
lines 116 - 134, The .fileName text can overflow and push buttons because flex
items need an explicit shrink/basis; update the CSS for .fileName (and/or
.selectedRow children) to allow proper shrinking by adding flex properties
(e.g., flex: 1 1 auto or flex: 1) and setting min-width: 0 on .fileName so the
ellipsis (text-overflow: ellipsis; white-space: nowrap) reliably truncates long
filenames without breaking the layout of .selectedRow.
frontend/src/components/AdminHome/RecentActivitiesPanel.jsx (1)

7-14: 활동 목록이 비어있을 때 빈 상태 UI 고려

activities가 빈 배열일 때 빈 <ul>만 렌더링됩니다. 사용자 경험을 위해 빈 상태 메시지를 표시하는 것이 좋습니다.

💡 빈 상태 처리 예시
       <ul className={styles.list}>
-        {activities.map((activity) => (
+        {activities.length === 0 ? (
+          <li className={styles.emptyText}>최근 활동이 없습니다.</li>
+        ) : activities.map((activity) => (
           <li key={activity.id} className={styles.listItemColumn}>
             <p className={styles.activityMessage}>{activity.message}</p>
             <p className={styles.memberMeta}>{activity.time}</p>
           </li>
         ))}
       </ul>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminHome/RecentActivitiesPanel.jsx` around lines 7 -
14, The RecentActivitiesPanel currently renders an empty <ul> when activities is
an empty array; update the component (RecentActivitiesPanel / the render that
maps over activities) to detect when activities.length === 0 and render a clear
empty-state UI (e.g., a <div> or <li> with a friendly message and optional icon
using styles.emptyState or styles.listItemEmpty) instead of the empty list so
users see a helpful message when there are no activities.
frontend/src/components/AdminHome/AdminSidebar.jsx (1)

73-85: 탭 전환에 Link 대신 NavLink 사용 고려

현재 탭 전환은 Link와 수동 클래스 조건으로 구현되어 있습니다. 네비게이션 메뉴와의 일관성을 위해 NavLink를 사용하는 것도 고려해볼 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminHome/AdminSidebar.jsx` around lines 73 - 85,
Replace the manual active-class logic using Link and isDevSection in
AdminSidebar with react-router-dom's NavLink: import NavLink, swap the two Link
elements for NavLink components (the ones rendering "회장용" and "개발자용"), and use
NavLink's className callback to apply styles.tabButton plus styles.tabActive
when the link is active instead of relying on isDevSection; ensure the
active-detection matches the same paths ("/admin" and "/admin/dev/logs") and
remove or stop depending on the manual isDevSection-based class toggling once
NavLink is used.
frontend/src/components/AdminHome/AdminHeader.jsx (2)

15-19: 검색 입력이 기능하지 않음

검색 입력에 onChange 핸들러나 상태가 없어서 현재 기능하지 않습니다. 의도적인 플레이스홀더라면 무시해도 되지만, 기능 구현이 필요하다면 상태 관리를 추가해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminHome/AdminHeader.jsx` around lines 15 - 19, The
search input in AdminHeader lacks state and an onChange handler; add local state
(e.g., const [searchQuery, setSearchQuery] = useState('')) inside the
AdminHeader component and wire an onChange handler (e.g., handleSearchChange)
that calls setSearchQuery(event.target.value) and optionally invokes a passed
prop callback (e.g., props.onSearch or onSearchChange) to lift the value up;
update the input to use value={searchQuery} and onChange={handleSearchChange} so
the search field becomes controlled and functional.

22-25: 알림 점(dot)이 항상 표시됨

알림 점이 무조건 렌더링되어 실제 알림이 없어도 표시됩니다. 실제 알림 존재 여부에 따라 조건부 렌더링하는 것이 좋습니다.

💡 조건부 렌더링 예시
-const AdminHeader = ({ title }) => {
+const AdminHeader = ({ title, hasNotifications = false }) => {
           <button type="button" className={styles.iconButton} aria-label="알림">
             <Bell size={16} />
-            <span className={styles.dot} />
+            {hasNotifications && <span className={styles.dot} />}
           </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminHome/AdminHeader.jsx` around lines 22 - 25, The
small notification dot (span with className styles.dot) is always rendered;
update AdminHeader.jsx to render that span conditionally based on a boolean
indicating unread notifications (e.g., prop or state named
hasUnreadNotifications / notificationCount > 0). In the AdminHeader component,
compute or accept a boolean (hasUnreadNotifications) from props or the store and
replace the unconditional <span className={styles.dot} /> with a conditional
render like: render the dot only when hasUnreadNotifications is true; ensure the
boolean is correctly derived (e.g., notificationCount > 0) and remains
accessible to the button that contains the Bell icon.
frontend/src/App.jsx (1)

21-21: 주석 처리된 라우트/임포트는 정리하는 편이 좋습니다.

죽은 코드 주석이 남아 있으면 라우팅 기준점을 파악하기 어려워집니다.

🧹 제안 수정안
-// import CheckInPage from './components/attendancemanage/qrmanagement/CheckInPage.jsx';
@@
-            {/* <Route path="/attendance/check-in" element={<CheckInPage />} /> */}

Also applies to: 74-74

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/App.jsx` at line 21, Remove the dead commented import and route
entries so the codebase has no leftover commented routing code: delete the
commented import line referencing CheckInPage (the commented "// import
CheckInPage from './components/attendancemanage/qrmanagement/CheckInPage.jsx';")
and any corresponding commented route at the later location, and ensure there
are no other stale references to CheckInPage in App.jsx (so imports and Route
definitions only include active components).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/components/AdminExcelUpload/AdminExcelUpload.jsx`:
- Around line 74-77: The resetFile function only clears React state
(setSelectedFile and setUploadResult) but must also clear the underlying <input
type="file"> value so selecting the same file again triggers onChange; add a ref
for the file input (e.g., fileInputRef via useRef) in the AdminExcelUpload
component and, inside resetFile, set fileInputRef.current.value = '' (guarding
for null) after resetting state so the native input is cleared as well.

In `@frontend/src/components/AdminExcelUpload/AdminExcelUploadHeader.jsx`:
- Around line 39-42: The button in AdminExcelUploadHeader.jsx calls
onDownloadTemplate directly which will throw if the prop is undefined; update
the component to defensively handle missing prop by invoking onDownloadTemplate
only when defined (e.g. guard in the onClick handler) and optionally disable the
button (or provide a default no-op) when onDownloadTemplate is not passed so
clicks are safe; reference the onDownloadTemplate prop and the button element
with className styles.ghostButton when making the change.

In `@frontend/src/components/AdminHome/DashboardStats.jsx`:
- Around line 1-13: DashboardStats currently assumes styles is provided and will
throw when accessing styles.statsGrid; update the component signature or
destructuring to provide a safe default for styles (e.g., styles = {}) so
references like styles.statsGrid, styles.card, styles.cardTitle,
styles.cardValue, and styles.cardDescription cannot cause a TypeError, and
optionally add PropTypes to declare styles as an object (or required) to make
the contract explicit.

In `@frontend/src/components/AdminHome/MembersPanel.jsx`:
- Around line 3-7: MembersPanel accesses styles directly (e.g.,
className={styles.panel}, styles.panelHeader, styles.panelTitle) which throws if
styles is undefined; fix by providing a safe default for the styles prop (for
example set styles = {} in the component signature or initialize a fallback
const like const safeStyles = styles || {}) and then use that safeStyles (or
keep using styles after defaulting) so className lookups never read from
undefined.

In `@frontend/src/components/AdminHome/RecentActivitiesPanel.jsx`:
- Around line 1-17: RecentActivitiesPanel can throw a TypeError when
props.styles is undefined; update the component to provide a safe default for
styles (e.g., set styles = {} in the RecentActivitiesPanel parameter or add
RecentActivitiesPanel.defaultProps = { styles: {} }) and ensure any usage like
className={styles.panel}, styles.panelHeader, styles.list,
styles.listItemColumn, styles.activityMessage, and styles.memberMeta will not
access properties on undefined; this guarantees the activities mapping remains
safe when callers omit the styles prop.

In `@frontend/src/components/AdminMemberApproval/AdminMemberApproval.jsx`:
- Line 231: The "신청일시" column currently renders a hardcoded "-" causing loss of
the application timestamp; update the AdminMemberApproval component so the table
cell(s) for that column bind to the actual timestamp field (e.g.,
member.createdAt or member.appliedAt) in the row rendering code instead of "-"
and format it (e.g., toLocaleString or a formatDate util) for display; make the
same change for the duplicate cells referenced around the second occurrence
(lines 259-262) so both table rows show the real application datetime.
- Around line 56-60: The filter assumes member.name, member.email, and
member.studentId are non-null strings and calls toLowerCase/includes which will
throw for null or non-string values; update the filtering in AdminMemberApproval
(the pendingMembers.filter using normalizedQuery) to guard/coerce each field
before calling string methods — e.g., treat missing values as empty strings or
String(...) and only call toLowerCase/includes on the resulting string (also
ensure studentId is converted to string before calling includes) so
null/undefined/non-string fields no longer break filtering or rendering.
- Line 23: SelectedIds can persist across searches/filters causing hidden
members to be acted on; update AdminMemberApproval to clear or sanitize
selections whenever the visible member list changes: in the effect or callback
that updates the displayed members (e.g., when searchQuery/filters/pagination or
the derived visibleMembers array updates), call setSelectedIds([]) or
setSelectedIds(prev => prev.filter(id => visibleIdsSet.has(id))) so only
currently visible IDs remain selected; ensure checkbox handlers still toggle
using selectedIds/setSelectedIds and apply the same fix to the other
selection-related logic referenced (the blocks around lines 64-77 and 192-210).

In `@frontend/src/components/AdminMemberApproval/AdminMemberApproval.module.css`:
- Around line 102-107: The .tableWrap CSS hides horizontal overflow causing
right-side action columns to be inaccessible on narrow screens; update the
.tableWrap rules (refer to the .tableWrap selector) to allow horizontal
scrolling instead of clipping (enable overflow-x:auto or overflow:auto and add
touch scrolling support) and ensure the inner table can widen (e.g., keep its
intrinsic/min-width) so the action column remains reachable on mobile.

In `@frontend/src/components/AdminMemberManage/AdminMemberManage.jsx`:
- Line 240: The table header label "가입일" does not match the cell value (the code
renders the member's generation), so update the JSX in AdminMemberManage.jsx to
make them consistent: either change the <th> text "가입일" to "기수" (or the correct
label) or change the cell renderer that currently outputs generation (e.g.,
member.generation or the map item rendering) to output the join date field
(e.g., member.joinDate/createdAt). Ensure you apply the same fix for the other
occurrence noted around the second instance (the cell at the other row/column
rendering, referenced near the code that uses generation).
- Line 249: The avatar initial render can throw if member.name is null/undefined
or not a string; update the AdminMemberManage.jsx avatar rendering (the div
using styles.avatar and member.name[0]) to defensively read the initial—use
optional chaining and a safe default (e.g., derive initial with something like
const initial = (typeof member?.name === 'string' && member.name.length) ?
member.name[0] : fallbackChar) and render that initial instead, or create a
small utility/getInitial function to centralize this logic so non-string or
missing names won't break rendering.
- Line 71: Remove the console.log that prints member data to the browser (the
console.log('회원 목록 로드 성공:', nextMembers) in the AdminMemberManage component) to
avoid exposing PII; if you still need runtime visibility for debugging, use a
stripped/aggregated log (e.g., log only counts or non-identifying metrics) or
wrap detailed logs behind a debug flag that is disabled in production and ensure
nextMembers is never logged in production builds.

In `@frontend/src/components/attendance/SessionManage.jsx`:
- Around line 115-116: Date column can show "-" when roundDate is missing
because display uses s.roundDate while sorting uses s.roundStartAt; change the
display to fall back to the start timestamp. Update the cell that renders
formatDate(s.roundDate) in SessionManage.jsx to use the start-time fallback
(e.g., call formatDate(s.roundDate ?? s.roundStartAt) or a small helper that
returns formatDate(roundDate) || formatDate(roundStartAt)), so the date column
shows a value when roundStartAt exists; keep formatTime(s.roundStartAt)
unchanged.

In `@frontend/src/pages/AdminHome.jsx`:
- Around line 25-28: loadAdminHomeData currently awaits getAdminHomeData without
error handling so a failed fetch leaves the dashboard blank; wrap the await in a
try/catch inside loadAdminHomeData, call setData with a safe fallback or empty
state on failure, set an error/loading state (or invoke existing
setError/setLoading handlers) and log the error (or show a user-facing message)
so failures surface instead of silently failing; locate loadAdminHomeData,
getAdminHomeData and setData in AdminHome.jsx to implement this.

In `@frontend/src/utils/adminHomeData.js`:
- Around line 17-56: The getAdminHomeData function lacks error handling around
the api.get('/api/admin/users') call; wrap the network call in a try/catch
inside getAdminHomeData, catch and log the error (e.g., console.error) and
return a safe default payload (dashboardStats with '-' or '0' as appropriate,
empty arrays for pendingApprovals, recentActivities, quickActions, members) so
the UI won't crash; alternatively rethrow a controlled error if you prefer
caller-handled errors, but ensure getAdminHomeData and its callers are
consistent.

In `@frontend/src/utils/adminMemberManageData.js`:
- Around line 35-54: Change both approvePendingMembersBulk and
rejectPendingMembersBulk to use Promise.allSettled instead of Promise.all,
iterate the settled results to collect userIds that failed, and if any failures
exist throw an Error object augmented with a failedUserIds array (e.g.,
error.failedUserIds = [...]) so callers (like AdminMemberApproval.jsx) can
inspect which userIds failed; keep the same api.patch calls and params in the
mapped promises but replace the final await Promise.all(...) with logic that
awaits Promise.allSettled(mappedPromises), filters for results with status !==
'fulfilled', builds the failed list from the corresponding userIds array, and
throws when non-empty (or return success when all fulfilled).

---

Outside diff comments:
In `@frontend/src/contexts/AuthContext.jsx`:
- Line 27: There's a typo in the variable name "paylaod" in AuthContext.jsx;
rename it to "payload" where it's declared (const paylaod = { studentId,
password }) and update all usages of "paylaod" in the surrounding
functions/methods (e.g., any calls or references that pass this object) to the
correct "payload" identifier to avoid ReferenceErrors.
- Around line 35-46: The logout function currently only calls
setIsLoggedIn(false) inside the catch block, so on a successful
api.post('/api/auth/logout') the UI stays logged in; move or add the state
update so setIsLoggedIn(false) runs regardless of API outcome (e.g., place it in
the finally block or immediately after the await) and ensure
localStorage.removeItem('user') remains executed; update the logout function
reference in AuthContext.jsx accordingly so that setIsLoggedIn is always cleared
when logout() completes.

---

Nitpick comments:
In `@frontend/src/App.jsx`:
- Line 21: Remove the dead commented import and route entries so the codebase
has no leftover commented routing code: delete the commented import line
referencing CheckInPage (the commented "// import CheckInPage from
'./components/attendancemanage/qrmanagement/CheckInPage.jsx';") and any
corresponding commented route at the later location, and ensure there are no
other stale references to CheckInPage in App.jsx (so imports and Route
definitions only include active components).

In `@frontend/src/components/AdminExcelUpload/AdminExcelUpload.module.css`:
- Around line 116-134: The .fileName text can overflow and push buttons because
flex items need an explicit shrink/basis; update the CSS for .fileName (and/or
.selectedRow children) to allow proper shrinking by adding flex properties
(e.g., flex: 1 1 auto or flex: 1) and setting min-width: 0 on .fileName so the
ellipsis (text-overflow: ellipsis; white-space: nowrap) reliably truncates long
filenames without breaking the layout of .selectedRow.

In `@frontend/src/components/AdminHome/AdminHeader.jsx`:
- Around line 15-19: The search input in AdminHeader lacks state and an onChange
handler; add local state (e.g., const [searchQuery, setSearchQuery] =
useState('')) inside the AdminHeader component and wire an onChange handler
(e.g., handleSearchChange) that calls setSearchQuery(event.target.value) and
optionally invokes a passed prop callback (e.g., props.onSearch or
onSearchChange) to lift the value up; update the input to use
value={searchQuery} and onChange={handleSearchChange} so the search field
becomes controlled and functional.
- Around line 22-25: The small notification dot (span with className styles.dot)
is always rendered; update AdminHeader.jsx to render that span conditionally
based on a boolean indicating unread notifications (e.g., prop or state named
hasUnreadNotifications / notificationCount > 0). In the AdminHeader component,
compute or accept a boolean (hasUnreadNotifications) from props or the store and
replace the unconditional <span className={styles.dot} /> with a conditional
render like: render the dot only when hasUnreadNotifications is true; ensure the
boolean is correctly derived (e.g., notificationCount > 0) and remains
accessible to the button that contains the Bell icon.

In `@frontend/src/components/AdminHome/AdminSidebar.jsx`:
- Around line 73-85: Replace the manual active-class logic using Link and
isDevSection in AdminSidebar with react-router-dom's NavLink: import NavLink,
swap the two Link elements for NavLink components (the ones rendering "회장용" and
"개발자용"), and use NavLink's className callback to apply styles.tabButton plus
styles.tabActive when the link is active instead of relying on isDevSection;
ensure the active-detection matches the same paths ("/admin" and
"/admin/dev/logs") and remove or stop depending on the manual isDevSection-based
class toggling once NavLink is used.

In `@frontend/src/components/AdminHome/MemberList.jsx`:
- Around line 8-25: Replace the inline styles in the MemberList component with
classNames from a CSS module: create (or import) a MemberList.module.css and
move table, thead, th, td and tr styles there, then update the JSX in
MemberList.jsx to use className attributes (e.g., className={styles.table},
styles.th, styles.td, styles.row) instead of the inline style objects used on
the <table>, <th>, <td>, and <tr> elements and keep the members.map rendering
logic intact.

In `@frontend/src/components/AdminHome/PendingApprovalsPanel.jsx`:
- Around line 8-31: handleApprove and handleReject allow repeated rapid clicks
because there is no "processing" state per member; add a local state (e.g.,
useState of a Set or map like processingIds) to track which member IDs are being
processed, check and early-return if the member.id is already in processing, add
the member.id to processingIds before the async call and remove it in a finally
block, and update the approve/reject buttons' disabled prop (and show a spinner
if desired) by checking processingIds.has(member.id) so the UI prevents
duplicate requests for the same member while the request is in-flight.
- Around line 42-68: The panel currently renders an empty list when
members.length === 0; update the PendingApprovalsPanel component to show a clear
empty-state message instead of an empty UL: add a conditional branch around the
members rendering so that when members.length === 0 you render a div or
paragraph (styled with a new or existing styles.emptyState) with a short message
like "대기 중인 회원이 없습니다" and optional helper text/CTA; keep the existing list
rendering (with members.map, listItem keys, and handlers handleApprove /
handleReject) for the non-empty case.

In `@frontend/src/components/AdminHome/RecentActivitiesPanel.jsx`:
- Around line 7-14: The RecentActivitiesPanel currently renders an empty <ul>
when activities is an empty array; update the component (RecentActivitiesPanel /
the render that maps over activities) to detect when activities.length === 0 and
render a clear empty-state UI (e.g., a <div> or <li> with a friendly message and
optional icon using styles.emptyState or styles.listItemEmpty) instead of the
empty list so users see a helpful message when there are no activities.

In `@frontend/src/components/AdminMemberManage/AdminMemberManage.module.css`:
- Around line 26-35: Add keyboard-focus-visible styles for the interactive form
controls to improve accessibility: update the CSS for .searchInput and
.filterSelect and any action button classes (e.g., .actionButton or the buttons
used in this component) to include a :focus-visible rule that provides a
high-contrast visible indicator (such as a distinct outline or box-shadow and no
outline: none) and preserves existing sizing/spacing; ensure the style is
applied where similar controls exist later in the file (the block referenced by
the review) so keyboard users can clearly see focus state across search, filter
and action controls.

In `@frontend/src/components/AdminRoute.jsx`:
- Around line 5-8: The isAdminRole function currently hardcodes the 'PRESIDENT'
string; extract that into a reusable policy constant (e.g., ADMIN_ROLES or
ADMIN_ROLE) and reference the constant from isAdminRole instead of the literal.
Create a top-level constant in the module (or import from a central config/env)
such as ADMIN_ROLES = ['PRESIDENT'] (or read from process.env/feature flag) and
change isAdminRole to check membership (use normalizedRole and
includes/contains) so role policy can be updated without editing AdminRoute.jsx.

In `@frontend/src/components/Sidebar.jsx`:
- Around line 33-50: The Sidebar currently calls /api/user/details in the
useEffect (checkAdminRole) causing duplicate requests; instead, expose and use
the role stored in AuthContext (e.g., add or use userRole in AuthContext state)
and remove the checkAdminRole effect and api.get from Sidebar; in Sidebar
replace the isPresident logic to derive from the context value (useAuth ->
userRole, then compute userRole?.toUpperCase() === 'PRESIDENT') so only
AuthContext performs the network call.

In `@frontend/src/pages/AdminExcelUpload.module.css`:
- Around line 1-33: Extract the duplicated CSS (.layout, .mainArea, .contentArea
and the media queries) from AdminExcelUpload.module.css and
AdminMemberApproval.module.css into a new shared stylesheet
AdminLayout.module.css; move those rules exactly as-is into
AdminLayout.module.css, then update the pages/components that import
AdminExcelUpload.module.css and AdminMemberApproval.module.css to also import
layoutStyles from '../../styles/AdminLayout.module.css' (or the appropriate
relative path) and replace usages of .layout, .mainArea, and .contentArea to use
layoutStyles.layout, layoutStyles.mainArea, layoutStyles.contentArea; keep any
page-specific rules in their original module files (or remove duplicates) so
only common layout rules remain in AdminLayout.module.css.

In `@frontend/src/utils/adminHomeData.js`:
- Around line 3-8: The current recentActivities constant is static placeholder
data; either add a clear TODO comment above recentActivities indicating it must
be replaced by the real API response when available, or replace the constant
with an async fetch function (e.g., fetchRecentActivities or
getRecentActivities) that calls your backend endpoint (e.g.,
'/api/recent-activities'), parses JSON, returns the array, and returns a safe
fallback (empty array) on error; update exports to expose the async function and
remove/stop using the hardcoded recentActivities constant.

In `@frontend/src/utils/adminUserApi.js`:
- Around line 4-38: This file duplicates endpoints already implemented in
adminMembersData.js; refactor by extracting shared API calls into a single
common module (e.g., adminUsersApi or reuse adminMembersData) and convert
functions here—getAdminUsers, updateAdminUserRole, updateAdminUserStatus,
promoteAdminUserSenior, deleteAdminUser—into thin wrappers that import and
re-export or delegate to the canonical implementations; ensure parameter names
and return shapes match the original functions and update callers to import the
shared module to avoid drift when endpoints or response formats change.

In `@frontend/src/utils/attendanceList.js`:
- Around line 3-12: The exported async function attendanceList should be renamed
to a clearer async name (e.g., fetchAttendanceList or getAttendanceList) to
signal it returns a Promise; update the function declaration/export from
attendanceList to the chosen name and update all call sites/imports that
reference attendanceList accordingly (search for attendanceList usages), leaving
the request logic (api.get('/api/attendance/me')) and error handling unchanged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f532c7 and 4ef0bd3.

📒 Files selected for processing (40)
  • frontend/src/App.jsx
  • frontend/src/components/AdminExcelUpload/AdminExcelUpload.jsx
  • frontend/src/components/AdminExcelUpload/AdminExcelUpload.module.css
  • frontend/src/components/AdminExcelUpload/AdminExcelUploadHeader.jsx
  • frontend/src/components/AdminHome/AdminHeader.jsx
  • frontend/src/components/AdminHome/AdminHeader.module.css
  • frontend/src/components/AdminHome/AdminSidebar.jsx
  • frontend/src/components/AdminHome/AdminSidebar.module.css
  • frontend/src/components/AdminHome/DashboardStats.jsx
  • frontend/src/components/AdminHome/MemberList.jsx
  • frontend/src/components/AdminHome/MembersPanel.jsx
  • frontend/src/components/AdminHome/PendingApprovalsPanel.jsx
  • frontend/src/components/AdminHome/QuickActionsPanel.jsx
  • frontend/src/components/AdminHome/RecentActivitiesPanel.jsx
  • frontend/src/components/AdminMemberApproval/AdminMemberApproval.jsx
  • frontend/src/components/AdminMemberApproval/AdminMemberApproval.module.css
  • frontend/src/components/AdminMemberManage/AdminMemberManage.jsx
  • frontend/src/components/AdminMemberManage/AdminMemberManage.module.css
  • frontend/src/components/AdminRoute.jsx
  • frontend/src/components/Sidebar.jsx
  • frontend/src/components/attendance/SessionManage.jsx
  • frontend/src/components/attendance/SessionManage.module.css
  • frontend/src/components/attendance/SessionSelectBox.jsx
  • frontend/src/components/attendance/SessionSelectBox.module.css
  • frontend/src/contexts/AuthContext.jsx
  • frontend/src/pages/AdminExcelUpload.jsx
  • frontend/src/pages/AdminExcelUpload.module.css
  • frontend/src/pages/AdminHome.jsx
  • frontend/src/pages/AdminHome.module.css
  • frontend/src/pages/AdminMemberApproval.jsx
  • frontend/src/pages/AdminMemberApproval.module.css
  • frontend/src/pages/AdminMemberManage.jsx
  • frontend/src/pages/AdminMemberManage.module.css
  • frontend/src/pages/Attendance.jsx
  • frontend/src/pages/Attendance.module.css
  • frontend/src/utils/adminHomeData.js
  • frontend/src/utils/adminMemberManageData.js
  • frontend/src/utils/adminMembersData.js
  • frontend/src/utils/adminUserApi.js
  • frontend/src/utils/attendanceList.js

Comment on lines +74 to +77
const resetFile = () => {
setSelectedFile(null);
setUploadResult(null);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd AdminExcelUpload.jsx

Repository: SISC-IT/sisc-web

Length of output: 161


🏁 Script executed:

wc -l frontend/src/components/AdminExcelUpload/AdminExcelUpload.jsx

Repository: SISC-IT/sisc-web

Length of output: 125


🏁 Script executed:

cat -n frontend/src/components/AdminExcelUpload/AdminExcelUpload.jsx

Repository: SISC-IT/sisc-web

Length of output: 5625


🌐 Web query:

HTML file input onChange event same file selection value reset behavior

💡 Result:

Selecting the same file again often doesn’t fire change (or input) on <input type="file"> because many browsers treat it as “no change”. The HTML Standard explicitly allows user agents to decide whether re-selecting the same files counts as a change or a dismissal, so behavior can vary by browser/version. [1]

Common, reliable workaround: clear the file input’s value before opening the picker (so any subsequent selection is “different” from an empty selection).

<input id="f" type="file">
<script>
  const f = document.getElementById('f');

  f.addEventListener('click', () => { f.value = ''; }); // or: f.value = null
  f.addEventListener('change', () => {
    console.log(f.files);   // handle files
    // optional: f.value = ''; // also fine to clear after processing
  });
</script>

This pattern is widely used specifically to make change fire even when the user picks the same file again. [2]

Alternatives (when clearing value isn’t enough in your setup):

  • Replace the input element with a fresh clone after handling the selection (forces a “new control” state). [3]
  • Reset the containing <form> (clears the file control as part of form reset). [4]

Note: seeing C:\fakepath\... in .value is expected (security feature); use input.files for the actual File objects. [2]

Sources:
[1] HTML Standard (WHATWG) – file picker change/dismissal is UA-defined, including re-selecting the same files
[2] Stack Overflow – reset input[type=file] value (e.g., this.value = null) on click to re-trigger change for same file
[3] Stack Overflow – workaround: clone/replace the file input to guarantee change firing
[4] Stack Overflow – workaround: reset the form to clear the file input


파일 입력 요소의 값을 초기화해야 동일 파일 재선택이 작동합니다.

resetFile에서 React 상태만 초기화하고 <input type="file">의 값을 초기화하지 않으면, 사용자가 같은 파일을 다시 선택할 때 onChange 이벤트가 발생하지 않습니다. HTML 파일 입력의 표준 동작으로, 입력 요소의 값이 변경되지 않으면 브라우저가 변화를 감지하지 않습니다.

🔧 제안 수정안
-import { useState } from 'react';
+import { useRef, useState } from 'react';

 const AdminExcelUpload = () => {
+  const fileInputRef = useRef(null);
   const [selectedFile, setSelectedFile] = useState(null);
   const [isUploading, setIsUploading] = useState(false);
   const [uploadResult, setUploadResult] = useState(null);
   const [isDragOver, setIsDragOver] = useState(false);
@@
   const resetFile = () => {
     setSelectedFile(null);
     setUploadResult(null);
+    if (fileInputRef.current) {
+      fileInputRef.current.value = '';
+    }
   };
@@
             <input
+              ref={fileInputRef}
               type="file"
               accept=".xlsx,.xls"
               onChange={handleInputChange}
               className={styles.fileInput}
             />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminExcelUpload/AdminExcelUpload.jsx` around lines
74 - 77, The resetFile function only clears React state (setSelectedFile and
setUploadResult) but must also clear the underlying <input type="file"> value so
selecting the same file again triggers onChange; add a ref for the file input
(e.g., fileInputRef via useRef) in the AdminExcelUpload component and, inside
resetFile, set fileInputRef.current.value = '' (guarding for null) after
resetting state so the native input is cleared as well.

Comment on lines +39 to +42
<button type="button" className={styles.ghostButton} onClick={onDownloadTemplate}>
<Download size={14} />
회원 등록 템플릿 다운로드 (.xlsx)
</button>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

onDownloadTemplate이 undefined일 경우 오류 가능

onDownloadTemplate prop이 전달되지 않으면 버튼 클릭 시 오류가 발생할 수 있습니다. 방어적 처리를 추가하는 것이 좋습니다.

🛡️ 제안된 수정
-const AdminExcelUploadHeader = ({ onDownloadTemplate }) => {
+const AdminExcelUploadHeader = ({ onDownloadTemplate = () => {} }) => {

또는 버튼에서 호출 시 검사:

-        <button type="button" className={styles.ghostButton} onClick={onDownloadTemplate}>
+        <button type="button" className={styles.ghostButton} onClick={() => onDownloadTemplate?.()}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button type="button" className={styles.ghostButton} onClick={onDownloadTemplate}>
<Download size={14} />
회원 등록 템플릿 다운로드 (.xlsx)
</button>
<button type="button" className={styles.ghostButton} onClick={() => onDownloadTemplate?.()}>
<Download size={14} />
회원 등록 템플릿 다운로드 (.xlsx)
</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminExcelUpload/AdminExcelUploadHeader.jsx` around
lines 39 - 42, The button in AdminExcelUploadHeader.jsx calls onDownloadTemplate
directly which will throw if the prop is undefined; update the component to
defensively handle missing prop by invoking onDownloadTemplate only when defined
(e.g. guard in the onClick handler) and optionally disable the button (or
provide a default no-op) when onDownloadTemplate is not passed so clicks are
safe; reference the onDownloadTemplate prop and the button element with
className styles.ghostButton when making the change.

Comment on lines +1 to +13
const DashboardStats = ({ stats = [], styles }) => {
return (
<div className={styles.statsGrid}>
{stats.map((stat) => (
<section key={stat.id} className={styles.card}>
<p className={styles.cardTitle}>{stat.title}</p>
<p className={styles.cardValue}>{stat.value}</p>
<p className={styles.cardDescription}>{stat.description}</p>
</section>
))}
</div>
);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

styles prop이 undefined일 경우 런타임 에러가 발생합니다.

stats는 기본값이 있지만 styles는 없습니다. styles가 전달되지 않으면 styles.statsGrid 접근 시 TypeError가 발생합니다.

🛡️ 기본값 추가 제안
-const DashboardStats = ({ stats = [], styles }) => {
+const DashboardStats = ({ stats = [], styles = {} }) => {

또는 PropTypes를 추가하여 필수 prop임을 명시할 수 있습니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const DashboardStats = ({ stats = [], styles }) => {
return (
<div className={styles.statsGrid}>
{stats.map((stat) => (
<section key={stat.id} className={styles.card}>
<p className={styles.cardTitle}>{stat.title}</p>
<p className={styles.cardValue}>{stat.value}</p>
<p className={styles.cardDescription}>{stat.description}</p>
</section>
))}
</div>
);
};
const DashboardStats = ({ stats = [], styles = {} }) => {
return (
<div className={styles.statsGrid}>
{stats.map((stat) => (
<section key={stat.id} className={styles.card}>
<p className={styles.cardTitle}>{stat.title}</p>
<p className={styles.cardValue}>{stat.value}</p>
<p className={styles.cardDescription}>{stat.description}</p>
</section>
))}
</div>
);
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminHome/DashboardStats.jsx` around lines 1 - 13,
DashboardStats currently assumes styles is provided and will throw when
accessing styles.statsGrid; update the component signature or destructuring to
provide a safe default for styles (e.g., styles = {}) so references like
styles.statsGrid, styles.card, styles.cardTitle, styles.cardValue, and
styles.cardDescription cannot cause a TypeError, and optionally add PropTypes to
declare styles as an object (or required) to make the contract explicit.

Comment on lines +3 to +7
const MembersPanel = ({ members = [], styles }) => {
return (
<section className={styles.panel}>
<div className={styles.panelHeader}>
<h2 className={styles.panelTitle}>회원 목록</h2>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

styles 미전달 시 렌더 단계에서 런타임 에러가 발생합니다

Line 5~7에서 styles를 직접 참조하고 있어, 부모에서 누락되면 Cannot read properties of undefined가 납니다. 기본값을 두어 안전하게 처리해 주세요.

🔧 제안 코드
-const MembersPanel = ({ members = [], styles }) => {
+const MembersPanel = ({ members = [], styles = {} }) => {
   return (
-    <section className={styles.panel}>
-      <div className={styles.panelHeader}>
-        <h2 className={styles.panelTitle}>회원 목록</h2>
+    <section className={styles.panel ?? ''}>
+      <div className={styles.panelHeader ?? ''}>
+        <h2 className={styles.panelTitle ?? ''}>회원 목록</h2>
       </div>
       <MemberList members={members} />
     </section>
   );
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const MembersPanel = ({ members = [], styles }) => {
return (
<section className={styles.panel}>
<div className={styles.panelHeader}>
<h2 className={styles.panelTitle}>회원 목록</h2>
const MembersPanel = ({ members = [], styles = {} }) => {
return (
<section className={styles.panel ?? ''}>
<div className={styles.panelHeader ?? ''}>
<h2 className={styles.panelTitle ?? ''}>회원 목록</h2>
</div>
<MemberList members={members} />
</section>
);
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminHome/MembersPanel.jsx` around lines 3 - 7,
MembersPanel accesses styles directly (e.g., className={styles.panel},
styles.panelHeader, styles.panelTitle) which throws if styles is undefined; fix
by providing a safe default for the styles prop (for example set styles = {} in
the component signature or initialize a fallback const like const safeStyles =
styles || {}) and then use that safeStyles (or keep using styles after
defaulting) so className lookups never read from undefined.

Comment on lines +1 to +17
const RecentActivitiesPanel = ({ activities = [], styles }) => {
return (
<section className={styles.panel}>
<div className={styles.panelHeader}>
<h2 className={styles.panelTitle}>최근 활동</h2>
</div>
<ul className={styles.list}>
{activities.map((activity) => (
<li key={activity.id} className={styles.listItemColumn}>
<p className={styles.activityMessage}>{activity.message}</p>
<p className={styles.memberMeta}>{activity.time}</p>
</li>
))}
</ul>
</section>
);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

styles prop이 undefined일 경우 런타임 오류 가능성

styles prop에 기본값이 없어서 호출자가 전달하지 않으면 className 접근 시 TypeError가 발생할 수 있습니다.

🛡️ 제안된 수정
-const RecentActivitiesPanel = ({ activities = [], styles }) => {
+const RecentActivitiesPanel = ({ activities = [], styles = {} }) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminHome/RecentActivitiesPanel.jsx` around lines 1 -
17, RecentActivitiesPanel can throw a TypeError when props.styles is undefined;
update the component to provide a safe default for styles (e.g., set styles = {}
in the RecentActivitiesPanel parameter or add RecentActivitiesPanel.defaultProps
= { styles: {} }) and ensure any usage like className={styles.panel},
styles.panelHeader, styles.list, styles.listItemColumn, styles.activityMessage,
and styles.memberMeta will not access properties on undefined; this guarantees
the activities mapping remains safe when callers omit the styles prop.

<tr key={member.id}>
<td>
<div className={styles.memberInfo}>
<div className={styles.avatar}>{member.name[0]}</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

아바타 이니셜 접근에서 런타임 에러 가능성이 있습니다.

member.name이 비어있거나 null인 응답이 오면 렌더링이 깨질 수 있습니다.

🔧 제안 수정안
-                    <div className={styles.avatar}>{member.name[0]}</div>
+                    <div className={styles.avatar}>{member.name?.[0] ?? '?'}</div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className={styles.avatar}>{member.name[0]}</div>
<div className={styles.avatar}>{member.name?.[0] ?? '?'}</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminMemberManage/AdminMemberManage.jsx` at line 249,
The avatar initial render can throw if member.name is null/undefined or not a
string; update the AdminMemberManage.jsx avatar rendering (the div using
styles.avatar and member.name[0]) to defensively read the initial—use optional
chaining and a safe default (e.g., derive initial with something like const
initial = (typeof member?.name === 'string' && member.name.length) ?
member.name[0] : fallbackChar) and render that initial instead, or create a
small utility/getInitial function to centralize this logic so non-string or
missing names won't break rendering.

Comment on lines +115 to +116
<td>{formatDate(s.roundDate)}</td>
<td>{formatTime(s.roundStartAt)}</td>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

일자 컬럼이 실제 데이터가 있어도 -로 표시될 수 있습니다.

정렬/회차 계산은 roundStartAt를 fallback으로 쓰는데, 표시는 roundDate만 사용해서 값 누락이 발생할 수 있습니다.

🔧 제안 수정안
-              <td>{formatDate(s.roundDate)}</td>
+              <td>{formatDate(s.roundDate || s.roundStartAt)}</td>
               <td>{formatTime(s.roundStartAt)}</td>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<td>{formatDate(s.roundDate)}</td>
<td>{formatTime(s.roundStartAt)}</td>
<td>{formatDate(s.roundDate || s.roundStartAt)}</td>
<td>{formatTime(s.roundStartAt)}</td>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/attendance/SessionManage.jsx` around lines 115 - 116,
Date column can show "-" when roundDate is missing because display uses
s.roundDate while sorting uses s.roundStartAt; change the display to fall back
to the start timestamp. Update the cell that renders formatDate(s.roundDate) in
SessionManage.jsx to use the start-time fallback (e.g., call
formatDate(s.roundDate ?? s.roundStartAt) or a small helper that returns
formatDate(roundDate) || formatDate(roundStartAt)), so the date column shows a
value when roundStartAt exists; keep formatTime(s.roundStartAt) unchanged.

Comment on lines +25 to +28
const loadAdminHomeData = async () => {
const adminHomeData = await getAdminHomeData();
setData(adminHomeData);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

데이터 로드 실패 시 예외 처리가 없어 대시보드가 침묵 실패할 수 있습니다.

getAdminHomeData() 실패를 처리하지 않아 초기 진입/새로고침 시 화면이 빈 상태로 남고 오류 맥락도 사라집니다.

🔧 제안 수정안
 const AdminHome = () => {
   const [data, setData] = useState({
@@
     quickActions: [],
     members: [],
   });
+  const [error, setError] = useState(null);

@@
   const loadAdminHomeData = async () => {
-    const adminHomeData = await getAdminHomeData();
-    setData(adminHomeData);
+    try {
+      setError(null);
+      const adminHomeData = await getAdminHomeData();
+      setData(adminHomeData);
+    } catch {
+      setError('관리자 데이터를 불러오지 못했습니다.');
+    }
   };

   return (
@@
         <AdminHeader title="관리자 대시보드" />
         <div className={styles.container}>
+          {error && <p role="alert">{error}</p>}
           <DashboardStats stats={data.dashboardStats} styles={styles} />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/AdminHome.jsx` around lines 25 - 28, loadAdminHomeData
currently awaits getAdminHomeData without error handling so a failed fetch
leaves the dashboard blank; wrap the await in a try/catch inside
loadAdminHomeData, call setData with a safe fallback or empty state on failure,
set an error/loading state (or invoke existing setError/setLoading handlers) and
log the error (or show a user-facing message) so failures surface instead of
silently failing; locate loadAdminHomeData, getAdminHomeData and setData in
AdminHome.jsx to implement this.

Comment on lines +17 to +56
export const getAdminHomeData = async () => {
const response = await api.get('/api/admin/users');
const users = response.data || [];

const pendingApprovals = users.filter((user) => user.role === 'PENDING_MEMBER');
const members = users.filter((user) => user.role !== 'PENDING_MEMBER');

return {
dashboardStats: [
{
id: 'members',
title: '총 회원 수',
value: String(members.length),
description: '승인 회원',
},
{
id: 'visitors',
title: '금일 방문자',
value: '-',
description: '집계 준비 중',
},
{
id: 'attendance',
title: '주간 출석률',
value: '-',
description: '집계 준비 중',
},
{
id: 'pending',
title: '승인 대기',
value: String(pendingApprovals.length),
description: '가입 신청',
},
],
pendingApprovals,
recentActivities,
quickActions,
members,
};
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

API 호출에 에러 처리가 없습니다.

api.get('/api/admin/users') 호출이 실패하면 예외가 호출자에게 전파되어 UI 크래시가 발생할 수 있습니다. 다른 유사한 유틸리티(attendanceList.js)처럼 try/catch로 에러를 처리하거나, 호출하는 컴포넌트에서 에러를 처리하도록 문서화해야 합니다.

🛡️ 에러 처리 추가 제안
 export const getAdminHomeData = async () => {
+  try {
     const response = await api.get('/api/admin/users');
     const users = response.data || [];

     const pendingApprovals = users.filter((user) => user.role === 'PENDING_MEMBER');
     const members = users.filter((user) => user.role !== 'PENDING_MEMBER');

     return {
       dashboardStats: [
         // ... stats
       ],
       pendingApprovals,
       recentActivities,
       quickActions,
       members,
     };
+  } catch (err) {
+    console.error('관리자 홈 데이터 불러오기 실패:', err);
+    throw err;
+  }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const getAdminHomeData = async () => {
const response = await api.get('/api/admin/users');
const users = response.data || [];
const pendingApprovals = users.filter((user) => user.role === 'PENDING_MEMBER');
const members = users.filter((user) => user.role !== 'PENDING_MEMBER');
return {
dashboardStats: [
{
id: 'members',
title: '총 회원 수',
value: String(members.length),
description: '승인 회원',
},
{
id: 'visitors',
title: '금일 방문자',
value: '-',
description: '집계 준비 중',
},
{
id: 'attendance',
title: '주간 출석률',
value: '-',
description: '집계 준비 중',
},
{
id: 'pending',
title: '승인 대기',
value: String(pendingApprovals.length),
description: '가입 신청',
},
],
pendingApprovals,
recentActivities,
quickActions,
members,
};
};
export const getAdminHomeData = async () => {
try {
const response = await api.get('/api/admin/users');
const users = response.data || [];
const pendingApprovals = users.filter((user) => user.role === 'PENDING_MEMBER');
const members = users.filter((user) => user.role !== 'PENDING_MEMBER');
return {
dashboardStats: [
{
id: 'members',
title: '총 회원 수',
value: String(members.length),
description: '승인 회원',
},
{
id: 'visitors',
title: '금일 방문자',
value: '-',
description: '집계 준비 중',
},
{
id: 'attendance',
title: '주간 출석률',
value: '-',
description: '집계 준비 중',
},
{
id: 'pending',
title: '승인 대기',
value: String(pendingApprovals.length),
description: '가입 신청',
},
],
pendingApprovals,
recentActivities,
quickActions,
members,
};
} catch (err) {
console.error('관리자 홈 데이터 불러오기 실패:', err);
throw err;
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/utils/adminHomeData.js` around lines 17 - 56, The
getAdminHomeData function lacks error handling around the
api.get('/api/admin/users') call; wrap the network call in a try/catch inside
getAdminHomeData, catch and log the error (e.g., console.error) and return a
safe default payload (dashboardStats with '-' or '0' as appropriate, empty
arrays for pendingApprovals, recentActivities, quickActions, members) so the UI
won't crash; alternatively rethrow a controlled error if you prefer
caller-handled errors, but ensure getAdminHomeData and its callers are
consistent.

Comment on lines +35 to +54
export const approvePendingMembersBulk = async ({ userIds }) => {
await Promise.all(
userIds.map((userId) =>
api.patch(`/api/admin/users/${userId}/role`, null, {
params: { role: 'TEAM_MEMBER' },
})
)
);
};

// 다중 회원 일괄 가입 거절
export const rejectPendingMembersBulk = async ({ userIds }) => {
await Promise.all(
userIds.map((userId) =>
api.patch(`/api/admin/users/${userId}/status`, null, {
params: { status: 'OUT' },
})
)
);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the actual implementation of these functions
find frontend/src -name "*adminMemberManageData*" -type f

Repository: SISC-IT/sisc-web

Length of output: 103


🏁 Script executed:

# Check the current implementation in the file
cat -n frontend/src/utils/adminMemberManageData.js | head -60

Repository: SISC-IT/sisc-web

Length of output: 1736


🏁 Script executed:

# Find all callers of these bulk functions to see how errors are handled
rg -nP --type=js --type=jsx "approvePendingMembersBulk|rejectPendingMembersBulk" frontend/src -C5

Repository: SISC-IT/sisc-web

Length of output: 87


🏁 Script executed:

# Find all callers of these bulk functions without jsx type
rg -n "approvePendingMembersBulk|rejectPendingMembersBulk" frontend/src --type=js -C5

Repository: SISC-IT/sisc-web

Length of output: 5444


🏁 Script executed:

# Also search for all JS files that might import these functions
rg -n "approvePendingMembersBulk|rejectPendingMembersBulk" frontend/src -C5 | head -100

Repository: SISC-IT/sisc-web

Length of output: 5444


🏁 Script executed:

# Get more context around the approvePendingMembersBulk call to see full error handling
sed -n '95,125p' frontend/src/components/AdminMemberApproval/AdminMemberApproval.jsx

Repository: SISC-IT/sisc-web

Length of output: 954


🏁 Script executed:

# Get more context around the rejectPendingMembersBulk call
sed -n '120,145p' frontend/src/components/AdminMemberApproval/AdminMemberApproval.jsx

Repository: SISC-IT/sisc-web

Length of output: 838


일괄 승인/거절에서 부분 성공 상태를 잃어버립니다

현재 Promise.all() 구조에서는 일부 요청만 성공하면 최종적으로 하나의 예외로 처리되어, 어떤 사용자들이 실제로 실패했는지 추적할 수 없습니다. 운영 관점에서 재시도나 사용자 안내가 어렵습니다.

Promise.allSettled() 로 변경하고 실패한 사용자 ID를 담아 전달하도록 개선이 필요합니다.

🔧 제안 코드 (부분 실패 식별)
 export const approvePendingMembersBulk = async ({ userIds }) => {
-  await Promise.all(
+  const results = await Promise.allSettled(
     userIds.map((userId) =>
       api.patch(`/api/admin/users/${userId}/role`, null, {
         params: { role: 'TEAM_MEMBER' },
       })
     )
   );
+
+  const failedUserIds = results
+    .map((result, index) => ({ result, userId: userIds[index] }))
+    .filter(({ result }) => result.status === 'rejected')
+    .map(({ userId }) => userId);
+
+  if (failedUserIds.length > 0) {
+    const error = new Error(`일괄 승인 실패: ${failedUserIds.length}건`);
+    error.failedUserIds = failedUserIds;
+    throw error;
+  }
 };
 
 export const rejectPendingMembersBulk = async ({ userIds }) => {
-  await Promise.all(
+  const results = await Promise.allSettled(
     userIds.map((userId) =>
       api.patch(`/api/admin/users/${userId}/status`, null, {
         params: { status: 'OUT' },
       })
     )
   );
+
+  const failedUserIds = results
+    .map((result, index) => ({ result, userId: userIds[index] }))
+    .filter(({ result }) => result.status === 'rejected')
+    .map(({ userId }) => userId);
+
+  if (failedUserIds.length > 0) {
+    const error = new Error(`일괄 거절 실패: ${failedUserIds.length}건`);
+    error.failedUserIds = failedUserIds;
+    throw error;
+  }
 };

이 변경 후 AdminMemberApproval.jsx의 catch 블록에서 error.failedUserIds 를 활용하여 실패한 사용자들을 구분 처리할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/utils/adminMemberManageData.js` around lines 35 - 54, Change
both approvePendingMembersBulk and rejectPendingMembersBulk to use
Promise.allSettled instead of Promise.all, iterate the settled results to
collect userIds that failed, and if any failures exist throw an Error object
augmented with a failedUserIds array (e.g., error.failedUserIds = [...]) so
callers (like AdminMemberApproval.jsx) can inspect which userIds failed; keep
the same api.patch calls and params in the mapped promises but replace the final
await Promise.all(...) with logic that awaits
Promise.allSettled(mappedPromises), filters for results with status !==
'fulfilled', builds the failed list from the corresponding userIds array, and
throws when non-empty (or return success when all fulfilled).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant